package genetic;

import genetic.evolution.Basic1;
import genetic.evolution.EvolutionaryStrategy;
import genetic.treenodes.Addition;
import genetic.treenodes.Constant;
import genetic.treenodes.Division;
import genetic.treenodes.IfEqualThenElse;
import genetic.treenodes.IfGreaterThenElse;
import genetic.treenodes.Multiplication;
import genetic.treenodes.Sinus;
import genetic.treenodes.Subtraction;
import genetic.treenodes.Variable;
import genetic.treevisualizer.METreeBranchRenderer;
import genetic.treevisualizer.METreeNodeRenderer;
import genetic.treevisualizer.TreeVisualizer;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import jxl.Cell;
import jxl.Sheet;
import jxl.Workbook;
import jxl.read.biff.BiffException;

import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

public class GPApplication extends Observable implements Runnable {

	public GPApplication() {
		tv = new TreeVisualizer();
		tv.setTreeBranchRenderer(new METreeBranchRenderer());
		tv.setTreeNodeRenderer(new METreeNodeRenderer());
		tv.setBranchStartWidth(10.0);
		tv.setArenaColor(Color.black);
		tv.setSide(500);
		tv.setRenderNodes(true);
		tv.setCircleDiminishFactor(0.75);
	}
	
	public static XYSeries loadFromExcel(String fileName, String varNameY) {
        XYSeries s = new XYSeries(varNameY);

        try {
            Workbook wb = Workbook.getWorkbook(new File(fileName));
            Sheet sh = wb.getSheet(0);

            Cell varCell = sh.findCell(varNameY);

            Cell col[] = sh.getColumn(varCell.getColumn());

            // keine TimeSeries: x ist generierte fortlaufende Nummer
            // besser: varx und vary als args der function
            // die erste Zeile des xls beinhaltet den Namen, ergo
            // bei 1 mit Import beginnen. Nummerierung der Datensaetze
            // beginnt dann auch bei eins. Im xls entspricht das Zeile 2.
            for (int i=0; i<col.length-1; i++) {
            	s.add(i, Double.parseDouble(col[i+1].getContents()));
            }
        } catch (IOException ex) {
            Logger.getLogger(DataSet.class.getName()).log(Level.SEVERE, null, ex);
        } catch (BiffException ex) {
            Logger.getLogger(DataSet.class.getName()).log(Level.SEVERE, null, ex);
        }

        return s;
    }

	private XYSeriesCollection dataSeries = new XYSeriesCollection();
	private ArrayList<DataSet> dataSets = null;
	private EvolutionaryStrategy evStrategy = null;
	private ArrayList<GPTreeNode> functionSet;
	private ArrayList<GPTreeNode> variableSet;
	private ArrayList<GPTreeNode> constantSet;
	
	private int generation = 0;

	private ArrayList<GeneticProgram> individuals = new ArrayList<GeneticProgram>();
	
	private int maxGenerations = 1;
	private String projectDir;
	
	private Properties settings = new Properties();
	
	private String targetVariableName = "y";
	private TreeVisualizer tv;

	public XYSeries calcBest() {
		XYSeries xys = new XYSeries("best indiv gen. " + generation);

		int x = 0;
		for (DataSet ds : dataSets) {
			double ev = individuals.get(0).evaluate(ds);
			//TODO ersatz von x++ durch echtes x
			xys.add(x++, ev);
		}
		
		return xys;
	}
	public void createRandomPopulation(int numOfIndividuals, int maxDepth) {
		// initiate population with ramped half
		for (int i = 0; i < numOfIndividuals / 2; i++) {
			individuals.add(new GeneticProgram(functionSet, variableSet, constantSet));
			individuals.get(i).createRandom(maxDepth, 0);
		}
		for (int i = numOfIndividuals / 2; i < numOfIndividuals; i++) {
			individuals.add(new GeneticProgram(functionSet, variableSet,constantSet));
			individuals.get(i).createRandom(maxDepth, 1);
		}

		generation = 0;
	}

	public XYSeries getTargetXYSeries() {
		return dataSeries.getSeries(settings.getProperty("TargetVar").trim());

	}
	
	public void evolveNewGeneration() {
		// reset error-sum in each individual
		for (GeneticProgram gp : individuals) {
			gp.resetError();
		}

		// calculate error sum for each individual
		for (GeneticProgram gp : individuals) {
			for (DataSet ds : dataSets) {
				gp.evaluateWithErrorCalculation(ds, ds.get(targetVariableName));
			}
		}

		individuals = evStrategy.evolve(individuals);

		if (isCreateImagesForBestIndividuals()) {
			writeImageFromGeneticProgram(individuals.get(0), projectDir
					+ "best_gen_" + generation + ".png");
		}
		if (isCreateImagesForFullPopulations()) {
			writeImagesFromCurrentPopulation(projectDir + "gen_"
					+ generation + "/");
		}

		generation++;
	}
	public BufferedImage getCurrentGPTreeImage() {
		return tv.renderTree(individuals.get(0).getRoot());
	}

	public XYSeriesCollection getDataSeries() {
		return dataSeries;
	}

	public boolean isCreateImagesForBestIndividuals() {
		return Boolean.parseBoolean(settings.getProperty("CreateImagesForBestIndividuals"));
	}

	public boolean isCreateImagesForFullPopulations() {
		return Boolean.parseBoolean(settings.getProperty("CreateImagesForFullPopulations"));
	}

	public void loadSettingsFromFile(File configFile) {
		projectDir = configFile.getParent()+"/";

		try {
			FileInputStream in = new FileInputStream(configFile);
			settings.load(in);
		} catch (Exception e) {
			e.printStackTrace();
		}
		setupPopulation();
	
	}

	@Override
	public void run() {
		for (int iGeneration = 0; iGeneration < maxGenerations; iGeneration++) {
			generation = iGeneration;
			evolveNewGeneration();
			// TODO: bestes Individuum in Datei speichern

			setChanged();
			notifyObservers();
			
			if (Thread.interrupted()) {
				break;
			}
		}
	}
	
	public int getProgress() {
		return (int)(generation*100/maxGenerations);
	}

	public void setEvStrategy(EvolutionaryStrategy evStrategy) {
		this.evStrategy = evStrategy;
	}

	public void setMaxGenerations(int maxGenerations) {
		this.maxGenerations = maxGenerations;
	}

	public void setTargetVar(String targetVar) {
		this.targetVariableName = targetVar;
	}

	private void setupPopulation() {
		functionSet = new ArrayList<GPTreeNode>();
		variableSet = new ArrayList<GPTreeNode>();
		constantSet = new ArrayList<GPTreeNode>();
		
		// setup functionSet
		String operators[] = settings.getProperty("Operators").split(",");
		for (int i=0; i<operators.length; i++) {
			if (operators[i].trim().equals("Addition")) {
				functionSet.add(new Addition());
			}
			else if (operators[i].trim().equals("Subtraction")) {
				functionSet.add(new Subtraction());
			}
			else if (operators[i].trim().equals("Multiplication")) {
				functionSet.add(new Multiplication());
			}
			else if (operators[i].trim().equals("Division")) {
				functionSet.add(new Division());
			}
			else if (operators[i].trim().equals("Sinus")) {
				functionSet.add(new Sinus());
			}
			else if (operators[i].trim().equals("IfEqualThenElse")) {
				functionSet.add(new IfEqualThenElse());
			}
			else if (operators[i].trim().equals("IfGreaterThenElse")) {
				functionSet.add(new IfGreaterThenElse());
			}
			else {
				System.err.println("property error: unknown operator ("+operators[i].trim()+")");
			}
		}
		// setup terminalSet & dataSet +targetVar
		double constantMin = Double.parseDouble(settings.getProperty("ConstantsRange").split(",")[0].trim());
		double constantMax = Double.parseDouble(settings.getProperty("ConstantsRange").split(",")[1].trim());
		
		for (int i=0; i<Integer.parseInt(settings.getProperty("NumOfRandomConstants").trim()); i++) {
			constantSet.add(new Constant(Math.random() * (constantMax-constantMin) + constantMin));
		}
		for (int i=0; i<settings.getProperty("InputVars").split(",").length; i++) {
			String varName = settings.getProperty("InputVars").split(",")[i].trim();
			variableSet.add(new Variable(varName));
			dataSeries.addSeries(GPApplication.loadFromExcel(projectDir+settings.getProperty("Datafile").trim(), varName));
		}
		dataSeries.addSeries(GPApplication.loadFromExcel(projectDir+settings.getProperty("Datafile").trim(), settings.getProperty("TargetVar").trim()));
		dataSets = DataSet.createListFromXYSeriesCollection(dataSeries);
		
		targetVariableName = settings.getProperty("TargetVar").trim();
		maxGenerations = Integer.parseInt(settings.getProperty("NumberOfGenerations"));

		createRandomPopulation(Integer.parseInt(settings.getProperty("PopulationSize")), Integer.parseInt(settings.getProperty("InitialMaxTreeDepth")));
		
		//TODO: select by config file
		evStrategy = new Basic1();
	}

	public void writeAllMaxDepths() {
		for (GeneticProgram gp : individuals) {
			System.out.println("i = " + individuals.indexOf(gp) + " :  "
					+ gp.findMaxTreeDepth());
		}
	}

	public void writeImageFromGeneticProgram(GeneticProgram gp, String filename) {
		tv.writeImageFile(tv.renderTree(gp.getRoot()), new File(filename));
	}

	public void writeImagesFromCurrentPopulation(String basename) {

		(new File(basename)).mkdir();
		for (GeneticProgram gp : individuals) {
			writeImageFromGeneticProgram(gp, basename + "indiv_"
					+ individuals.indexOf(gp) + ".png");
		}
	}

}
